package logic;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.util.AbstractMap.SimpleEntry;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Pattern;
import org.mozilla.universalchardet.ReaderFactory;
import com.spire.doc.Document;
import com.spire.doc.FileFormat;
import com.spire.pdf.PdfDocument;
import com.spire.pdf.widget.PdfPageCollection;
import com.spire.presentation.IAutoShape;
import com.spire.presentation.ISlide;
import com.spire.presentation.ParagraphEx;
import com.spire.presentation.Presentation;
import com.spire.xls.CellRange;
import com.spire.xls.Workbook;
import com.spire.xls.Worksheet;
import com.spire.xls.collections.WorksheetsCollection;
import qw.easyFrame.tools.statics.StringUtil;
import static java.util.Locale.ENGLISH;
import static qw.easyFrame.tools.statics.StringUtil.endsWith;

/**
 * 文本文件批量检索工具.<br>
 * 扫描文件夹内的文本文件，检索出指定的字符串。
 * @author Rex
 *
 */
public class TextFinder implements Serializable {
	private static final long serialVersionUID = 20240829L;
	
	/**
	 * 待检索的文件或文件夹.
	 */
	private File dir;
	
	/**
	 * 需要检索的扩展名（忽略大小写）.<br>
	 * 若不设置此项，则检索所有文件。
	 */
	private String[] extensions;
	
	/**
	 * 指定字符集，可不设置此项.
	 */
	private String charset;
	
	/**
	 * 目标字符串.
	 */
	private String target;
	
	/**
	 * 是否忽略大小写（仅在正则匹配模式下启用）.
	 */
	private boolean ignoreCase;
	
	private Pattern pattern;
	
	/**
	 * 检索文件内容时，是否忽略大小写（默认为false）.<br>
	 * 若此项为空，表示区分大小写。反之，表示忽略大小写，
	 * 且此变量的值是target变量调用toLowCase()方法的返回值。
	 * 注意：此项仅适用检索文件内容，不适用匹配文件扩展名。匹配文件扩展名始终忽略大小写。
	 * @see TextFinder#target
	 */
	private String targetLowCase;
	
	public TextFinder() {
		super();
	}
	
	/**
	 * 待检索的文件或文件夹.
	 * @param dir
	 */
	public void setDir(File dir) {
		this.dir = dir;
	}
	
	/**
	 * 设置需要检索的扩展名（忽略大小写）.<br>
	 * 注意：建议传入的扩展名包含小数点。<br>
	 * 若不设置此项（extensions为null），则检索所有文件。
	 *  若设置此项为空数组，则不检索任何文件。
	 * @param extensions
	 */
	public void setExtensions(String... extensions) {
		int len = extensions.length;
		this.extensions = new String[len];
		for (int i = 0; i < len; i++)
			this.extensions[i] = extensions[i].toLowerCase(ENGLISH);
	}

	public void setCharset(String charset) {
		this.charset = charset;
	}
	
	/**
	 * 检索文件内容时，是否忽略大小写.<br>
	 * 注意：此项仅适用检索文件内容，不适用匹配文件扩展名。匹配文件扩展名始终忽略大小写。
	 * @param target
	 * @param ignoreCase
	 */
	public void setTarget(String target, boolean ignoreCase) {
		this.target = target;
		if (ignoreCase)
			this.targetLowCase = target.toLowerCase(ENGLISH);
	}
	
	/**
	 * 检索文件内容时，是否忽略大小写.<br>
	 * 注意：此项仅适用检索文件内容，不适用匹配文件扩展名。匹配文件扩展名始终忽略大小写。
	 * @param pattern
	 * @param ignoreCase
	 */
	public void setTarget(Pattern pattern, boolean ignoreCase) {
		this.pattern = pattern;
		this.ignoreCase = ignoreCase;
	}

	/**
	 * 开始检索并返回检索结果.<br>
	 * @return key为匹配到指定字符串的文本文件；<br>
	 *         value为指定字符在文本文件中出现的行数（从1开始）。
	 */
	public List<SimpleEntry<String, Integer>> find() {
		List<SimpleEntry<String, Integer>> list = new LinkedList<>();

		flag: for (File file : search(dir, false)) {
			String fname = file.getAbsolutePath();
			if (fname.endsWith(".doc") || fname.endsWith(".docx")) { // word文档
				Document doc = null;
				try {
					//加载Word文档
			        doc = new Document();
			        doc.loadFromFile(fname, FileFormat.Auto);
			        //获取文档文本
			        String[] split = StringUtil.split(doc.getText(), "\r\n");
			        
					int last = split.length+1;
					for (int i = 1; i < last;)
						i = match(list, fname, split[i - 1], i);
					
				} catch (Exception e) {
					errMsg(list, fname);
					continue flag;
				} finally {
			        doc.close();
			        doc.dispose();
				}
			} else if (fname.endsWith(".xls") || fname.endsWith(".xlsx")){ // excel文档
				Workbook wb = new Workbook();
				try {
					// 加载Excel文件
					wb.loadFromFile(fname);

					// 获取工作表
					WorksheetsCollection shts = wb.getWorksheets();
					int count = shts.getCount();
					for (int index = 0; index < count; index++) {
						StringBuilder sb = new StringBuilder();
						Worksheet sheet = shts.get(index);
						CellRange range = sheet.getAllocatedRange();

						// 遍历行和列
						int rowLen = range.getRows().length;
						for (int i = 0; i < rowLen; i++) {
							int columnCount = range.getColumnCount();
							for (int j = 0; j < columnCount; j++)
								sb.append(range.get(i + 1, j + 1).getValue());
						}
						match(list, fname, sb.toString(), index + 1);
					}
				} catch (Exception e) {
					errMsg(list, fname);
					continue flag;
				} finally {
					wb.dispose();
				}
			} else if (fname.endsWith(".pdf")) { // PDF文档
				PdfDocument pdf = new PdfDocument();
				try {
					pdf.loadFromFile(fname); // PDF文件路径

					// 遍历PDF的每一页
					PdfPageCollection pages = pdf.getPages();
					int count = pages.getCount();

					for (int i = 0; i < count; i++) {
						String text = pages.get(i).extractText();
						match(list, fname, text, i + 1);
					}
				} catch (Exception e) {
					errMsg(list, fname);
					continue flag;
				} finally {
					pdf.close();
					pdf.dispose();
				}
			} else if (fname.endsWith(".ppt") || fname.endsWith(".pptx")) { // PPT文档
				try {
					Presentation ppt = new Presentation();
					// 加载PowerPoint文档
					ppt.loadFromFile(fname);

					// 遍历文档中的幻灯片，提取文本
					int i = 1;
					for (Object slide : ppt.getSlides()) {
						StringBuilder buffer = new StringBuilder();
						for (Object shape : ((ISlide) slide).getShapes()) {
							if (shape instanceof IAutoShape) {
								for (Object tp : ((IAutoShape) shape).getTextFrame().getParagraphs()) {
									buffer.append(((ParagraphEx) tp).getText());
								}
							}
						}
						String text = buffer.toString();
						i = match(list, fname, text, i);
					}
				} catch (Exception e) {
					errMsg(list, fname);
					continue flag;
				}
			} else { // 普通文本文件
				try (BufferedReader reader = charset == null ? ReaderFactory.createBufferedReader(file)
						: new BufferedReader(new InputStreamReader(new FileInputStream(file), charset))) {
					String line = null;
					int index = 1;
					while ((line = reader.readLine()) != null)
						index = match(list, fname, line, index);

				} catch (Exception e) {
					errMsg(list, fname);
					continue flag;
				}
			}
		}
		
		return list;
	}

	/**
	 * 添加错误信息.
	 * @param list
	 * @param fname
	 */
	private final static void errMsg(List<SimpleEntry<String, Integer>> list, String fname) {
		list.add(0, new SimpleEntry<String, Integer>("注意：" + fname + " 读取失败，请检查", -1));
	}

	/**
	 * 开始查找.
	 * @param list
	 * @param file
	 * @param line
	 * @param index
	 * @return
	 */
	private final int match(List<SimpleEntry<String, Integer>> list, String file, String line, int index) {
		// 正则模式
		if (pattern != null) {
			if (ignoreCase)
				line = line.toLowerCase(ENGLISH);
			
			if (pattern.matcher(line).matches()) 
				list.add(new SimpleEntry<>(file, index));
			
		} else {// 普通模式
			if (targetLowCase == null) {
				// 区分大小写
				if (line.indexOf(target, 0) > -1)
					list.add(new SimpleEntry<>(file, index));
			} else {
				// 忽略大小写
				if (line.toLowerCase(ENGLISH).indexOf(targetLowCase, 0) > -1)
					list.add(new SimpleEntry<>(file, index));
			}
		}

		return ++index;
	}
	
	/**
	 * 按照指定扩展名搜索文件.
	 * @param file
	 * @param isDirectory 当且仅当完全确定file参数为可访问的文件夹时，才为true
	 * @return
	 */
	private final List<File> search(File file, boolean isDirectory) {
		List<File> list = new LinkedList<>();

		if (isDirectory || file.isDirectory())
			for (File f : file.listFiles())
				if (f.isDirectory())
					list.addAll(search(f, true));
				else
					match(list, f, extensions);
		else if (file.isFile())
			match(list, file, extensions);

		return list;
	}
	
	/**
	 * 按照扩展名匹配文件.
	 * @param list
	 * @param f
	 * @param extensions
	 */
	private static final void match(List<File> list, File f, String[] extensions) {
		/*
		 *  若未指定扩展名（extensions为null），则检索所有文件（判断条件始终为true）。
		 *  若extensions为空数组，则不检索任何文件（判断条件始终为false）。
		 */
		if (extensions == null || endsWith(f.getName().toLowerCase(ENGLISH), extensions))
			list.add(f);
	}
	
}